﻿using System;
using System.Diagnostics.Contracts;

namespace O1.Data.Traversal
{
    [ContractVerification(true)]
    public class CellBuilder : Filter
    {
        private byte[] buffer;

        private int position;

        private int vertexCheckpoint;

        private int edgeCheckpoint;

        public CellBuilder()
            : this(256)
        {
        }

        public CellBuilder(int initialCapacity)
        {
            Contract.Ensures(this.buffer != null);
            Contract.Ensures(this.buffer.Length >= Sz.CLR.Int32);
            Contract.Ensures(this.position > -1);

            this.buffer = new byte[Math.Max(initialCapacity, Sz.CLR.Int32)];
        }

        public int Length
        {
            get
            {
                return this.position;
            }
        }

        public override ObservationInterests EndEdge(ObservationInterests edgeState)
        {
            if (edgeState == ObservationInterests.Interested)
            {
                this.CheckBuffer(1);
                this.buffer[this.position] = Markup.EdgeEnd;
                this.position++;
                this.edgeCheckpoint = 0;
            }
            else
            {
                if (this.edgeCheckpoint == 0)
                {
                    throw new InvalidOperationException(ExceptionText.EdgeEndWith0Offest);
                }

                Contract.Assume(this.edgeCheckpoint < this.position);
                this.position = this.edgeCheckpoint;
            }

            return ObservationInterests.Interested;
        }

        public override ObservationInterests EndVertex(ObservationInterests vertexState)
        {
            if (vertexState == ObservationInterests.Interested)
            {
                this.CheckBuffer(1);
                this.buffer[this.position] = Markup.VertexEnd;
                this.position++;
            }
            else
            {
                this.position = this.vertexCheckpoint;
            }

            return ObservationInterests.Interested;
        }

        public override ObservationInterests Evaluate(DateProperty value)
        {
            this.CheckBuffer(DateProperty.Size + 1);
            var p = this.position;
            this.buffer[p] = Markup.DateProp;
            value.Write(this.buffer, p + 1);
            this.position = p + DateProperty.Size + 1;
            return ObservationInterests.Interested;
        }

        public override ObservationInterests Evaluate(DateTimeProperty value)
        {
            this.CheckBuffer(DateTimeProperty.Size + 1);
            var p = this.position;
            this.buffer[p] = Markup.DateTimeProp;
            value.Write(this.buffer, p + 1);
            this.position = p + DateTimeProperty.Size + 1;
            return ObservationInterests.Interested;
        }

        public override ObservationInterests Evaluate(DecimalProperty value)
        {
            this.CheckBuffer(DecimalProperty.Size + 1);
            var p = this.position;
            this.buffer[p] = Markup.DecimalProp;
            value.Write(this.buffer, p + 1);
            this.position = p + DecimalProperty.Size + 1;
            return ObservationInterests.Interested;
        }

        public override ObservationInterests Evaluate(HashProperty value)
        {
            this.CheckBuffer(HashProperty.Size + 1);
            var p = this.position;
            this.buffer[p] = Markup.HashProp;
            value.Write(this.buffer, p + 1);
            this.position = p + HashProperty.Size + 1;
            return ObservationInterests.Interested;
        }

        public override ObservationInterests Evaluate(Int16Property value)
        {
            this.CheckBuffer(Int16Property.Size + 1);
            var p = this.position;
            this.buffer[p] = Markup.Int16Prop;
            value.Write(this.buffer, p + 1);
            this.position = p + Int16Property.Size + 1;
            return ObservationInterests.Interested;
        }

        public override ObservationInterests Evaluate(Int32Property value)
        {
            this.CheckBuffer(Int32Property.Size + 1);
            var p = this.position;
            this.buffer[p] = Markup.Int32Prop;
            value.Write(this.buffer, p + 1);
            this.position = p + Int32Property.Size + 1;
            return ObservationInterests.Interested;
        }

        public override ObservationInterests Evaluate(Int64Property value)
        {
            this.CheckBuffer(Int64Property.Size + 1);
            var p = this.position;
            this.buffer[p] = Markup.Int64Prop;
            value.Write(this.buffer, p + 1);
            this.position = p + Int64Property.Size + 1;
            return ObservationInterests.Interested;
        }

        public override ObservationInterests Evaluate(StringProperty value)
        {
            var size = value.GetSize();
            this.CheckBuffer(size + 1);
            var p = this.position;
            this.buffer[p] = Markup.StringProp;
            value.Write(this.buffer, p + 1);
            this.position = p + size + 1;
            return ObservationInterests.Interested;
        }

        public override ObservationInterests CheckVersion(CellVersion version)
        {
            this.CheckBuffer(CellVersion.Size);
            Contract.Assert(this.position + CellVersion.Size < this.buffer.Length);
            version.Write(this.buffer, this.position);
            this.position += CellVersion.Size;
            return ObservationInterests.Interested;
        }

        public override ObservationInterests StartVertex(CellAddress vertex)
        {
            this.vertexCheckpoint = this.position;
            this.CheckBuffer(Sz.CLR.Int32);

            Contract.Assert(this.position + Sz.CLR.Int32 < this.buffer.Length);
            CellAddress.WriteLocalId(vertex.CellId, this.buffer, this.position);
            this.position += Sz.CLR.Int32;
            return ObservationInterests.Interested;
        }

        public override ObservationInterests StartEdge(Edge edge, EdgeDirections direction)
        {
            Contract.Ensures(this.edgeCheckpoint < this.position);

            this.edgeCheckpoint = this.position;

            this.CheckBuffer(Edge.Size + 1);
            var p = this.position;
            this.buffer[p] = (byte)(direction == EdgeDirections.Inbound ? Markup.InEdge : Markup.OutEdge);
            edge.Write(this.buffer, p + 1);
            this.position = p + Edge.Size + 1;

            Contract.Assume(this.position > this.edgeCheckpoint);
            return ObservationInterests.Interested;
        }

        public byte[] ToArray()
        {
            var result = new byte[this.position];
            Array.Copy(this.buffer, result, this.position);
            return result;
        }

        public byte[] GetBuffer()
        {
            return this.buffer;
        }

        private void CheckBuffer(int nextSize)
        {
            Contract.Ensures(this.buffer != null);
            Contract.Ensures(this.buffer.Length > this.position + nextSize);

            if (this.buffer.Length <= this.position + nextSize)
            {
                var newSize = Math.Max(nextSize + this.position, this.buffer.Length * 2);

                Contract.Assume(nextSize > this.buffer.Length);
                var newBuffer = new byte[newSize];
                Array.Copy(this.buffer, newBuffer, this.position);
                this.buffer = newBuffer;

                Contract.Assume(this.buffer.Length > this.position + nextSize);
            }
        }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(this.buffer != null);
            Contract.Invariant(this.position > -1);
            Contract.Invariant(this.vertexCheckpoint > -1);
            Contract.Invariant(this.edgeCheckpoint > -1);
            Contract.Invariant(this.position < this.buffer.Length);
            Contract.Invariant(this.edgeCheckpoint <= this.buffer.Length);
            Contract.Invariant(this.vertexCheckpoint <= this.position);
            Contract.Invariant(this.vertexCheckpoint < this.edgeCheckpoint || this.edgeCheckpoint == 0);
        }
    }
}
